#ifndef _SELECT_SVR_H_
#define _SELECT_SVR_H_
#include <iostream>
#include <sys/select.h>
#include <string>
#include <sys/time.h>
#include <vector>

// #include "Log.hpp"
#include "Sock.hpp"
using namespace std;

#define Num (sizeof(fd_set) * 8)
#define FD_NONE -1

//select 不用多进程或者多线程等方法，都可以做到不阻塞的通信，因为select只有当数据到来时候才会去进行IO，所以每次一进行IO都会直接将
//数据拿上来了，所以不会进行阻塞的IO.


// select 目前只完成读取，写入和异常不做处理 --epoll(写完整版)
class SelectServer
{
public:
    SelectServer(const uint16_t &port = 8080) : _port(port) // 初始化select服务器
    {
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);
        ClearyFd(); // 清空文件描述符表
        fd_array[0] = _listensock;
    }
    void Start() // 开始用select监视一批套接字/文件描述符
    {
        while (true) // 不断监视一批套接字
        {
            uint16_t port;
            string ip;
            fd_set readfds; // 若i号文件描述符就绪，则readfds的第i个比特位就为真
            FD_ZERO(&readfds);

            DebugPrint(); // 检查fd_array当前有哪些文件描述符

            // 1.accept，用 单路的方法&&阻塞等待 对方的连接到来，再获取对方链接上来。(accpet当成了input,链接到来当成了数据就绪)
            //  int serversock=Sock::Accept(_listensock,&ip,&port);

            // 2.select，用多路转接的方法 多路&&(阻塞等待 or 非阻塞等待 or timeout) 来批量等待对方链接到来，再获取链接上来。
            //(select当成了input,链接到阿里当成了数据就绪)
            // select充当的是一个数据监视器，监视每个文件描述符是否就绪（是否有数据到来-监听套接字：是否有链接到来，服务套接字：是否有数据到来）

            int maxfd = _listensock;
            for (int i = 0; i < Num; i++)
            {
                if (fd_array[i] == FD_NONE)
                    continue;
                FD_SET(fd_array[i], &readfds); // 设置关心的文件描述符
                if (fd_array[i] > maxfd)
                    maxfd = fd_array[i];
            }

            struct timeval timeout = {0, 0};

            // 1. nfds: 随着我们获取的sock越来越多，随着我们添加到select的sock越来越多，注定了nfds每一次都可能要变化,我们需要对它动态计算
            // 2. rfds/writefds/exceptfds：都是输入输出型参数，输入输出不一定以一样的，所以注定了我们每一次都要对rfds进行重新添加
            // 3. timeout: 都是输入输出型参数，每一次都要进行重置，前提是你要的话
            // 1,2 => 注定了我们必须自己将合法的文件描述符需要单独全部保存起来 用来支持：1. 更新最大fd 2.更新位图结构
            int n = select(maxfd + 1, &readfds, nullptr, nullptr, nullptr); // 阻塞式等待
            // timeval参数 为nullptr时阻塞等待 ，为{0,0}时非阻塞等待 , 为{x,y}时按等待x s.y ms式等待
            switch (n) // 获取的文件描述符的数目
            {
            case 0:
                logMessage(DEBUG, "time out...");
                break;
            case -1:
                logMessage(WARNING, "select server error...");
                break;
            default:
                logMessage(DEBUG, "get some event...");
                HandlerEvent(readfds); // 处理事件
                break;
            }
        }
    }
    ~SelectServer() {}

private:
    void ClearyFd()
    {
        for (int i = 0; i < Num; i++)
        {
            fd_array[i] = FD_NONE;
        }
    }
    void HandlerEvent(const fd_set &readfds)
    {
        for (int i = 0; i < Num; i++)
        {
            if (fd_array[i] == FD_NONE)
                continue;

            if (FD_ISSET(fd_array[i], &readfds)) // 有文件描述符就绪
            {
                if (fd_array[i] == _listensock)
                    Accepter(); // 监听套接字文件描述符就绪
                else
                    Recever(i); // 服务套接字文件描述符就绪
            }
        }
    }

    void Accepter() // 获取链接，并发配服务套接字，并把新的服务套接字放入fd_array中，一个服务套接字对应一个链接。listensock一次只获取一个链接
    {
        string clientip;
        uint16_t clientport = 0;

        int sock = Sock::Accept(_listensock, &clientip, &clientport);
        if (sock < 0)
        {
            logMessage(ERROR, "accept error");
            return;
        }
        logMessage(DEBUG, "accept success,[%s:%d]", clientip.c_str(), clientport);
        // 获取上来链接之后，分配好了服务端套接字sock。但当前还不能做read和recv。因为这里是主进程，如果read和recv IO的话会有阻塞的可能。

        // 当前派发的服务套接字sock先放入数组中,让select去监视数据的到来，当数据到来之后再调用服务套接字去获取数据，此时就不会阻塞了。
        // 先将服务套接字sock放进fd_array数组

        int i = 1; // 找空位置放服务套接字sock
        for (i = 1; i < Num; i++)
        {
            if (fd_array[i] == FD_NONE)
                break;
        }
        if (i == Num)
        {
            logMessage(WARNING, "fd_array is full,close %d", sock);
            close(sock);
            return;
        }
        cout << "debug" << endl;
        fd_array[i] = sock;
    }

    void Recever(int pos) // 获取(接收)数据,由服务套接字sock完成
    {
        // 进行数据的接收

        logMessage(NORMAL, "data is coming...,get IO event[sock:%d]",fd_array[pos]);

        char buffer[1024];
        // recvfrom关系发来的数据，及发来的port、ip、family.而recv只关心发来的数据，因为accept已经获得过了port,ip等信息了
        int n = recv(fd_array[pos], buffer, sizeof(buffer) - 1, 0); // 最后一个位置用来放'\0'
        // 暂时先不做封装, 此时select已经帮我们进行了事件检测，fd上的数据一定是就绪的，即 本次 不会被阻塞
        // 这样读取有bug吗？有的，你怎么保证以读到了一个完整包文呢？epoll时解决


        if (n > 0)
        {
            buffer[n - 1] = '\0';
            logMessage(DEBUG, "client[sock:%d]# %s", fd_array[pos], buffer);
        }
        else if (n == 0)
        {
            logMessage(ERROR, "client quit...,so serversock is quit,too");
            // 1. 我们也要关闭不需要的fd
            close(fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            fd_array[pos] = FD_NONE;
        }
        else
        {
            // 1. 我们也要关闭不需要的fd
            close(fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            fd_array[pos] = FD_NONE;
            logMessage(WARNING, "%d serversock recv error, %d : %s", fd_array[pos], errno, strerror(errno));
        }
    }

    void DebugPrint()
    {
        cout << "fd_array: ";
        for (int i = 0; i < Num; i++)
        {
            if (fd_array[i] == FD_NONE)
                continue;
            cout << fd_array[i] << ' ';
        }
        cout << endl;
    }

private:
    uint16_t _port;
    int _listensock;
    int fd_array[Num];//用来存放套接字，数组元素的值就是文件描述符的值，第几号文件描述符。用来存放3~(1024+3)的文件描述符，
                      // 因为fd_set只有1024个位，所以只能标记1024个套接字，所以我们这也就只需要1024个大小的数组
};

#endif





